package us.luosl.lslt.algorithm;

import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.IntStream;

/**
 * 常用的距离和相似度计算工具类
 * Created by luosl on 2019/6/24.
 */

public class DistanceAndSimilarity {

    private static void check(List<Double> vec1, List<Double> vec2){
        assert null != vec1 && null != vec2;
        assert vec1.size() == vec2.size();
    }

    private static double calDistance(List<Double> vec1,
                                      List<Double> vec2,
                                      BiFunction<Double, Double, Double> sumCellProcessFun,
                                      Function<Double, Double> sumValueProcessFun){ 
        check(vec1, vec2);
        double sumValue = IntStream.range(0, vec1.size())
                .mapToDouble(idx -> sumCellProcessFun.apply(vec1.get(idx), vec2.get(idx)))
                .sum();
        return sumValueProcessFun.apply(sumValue);
    }

    /**
     * 欧几里得距离
     * @param vec1 向量1
     * @param vec2 向量2
     * @return double
     */
    public static double euclideanDistance(List<Double> vec1, List<Double> vec2){
        return calDistance(vec1, vec2, (x, y) -> Math.pow(x - y, 2), Math::sqrt);
    }

    /**
     * 曼哈顿距离
     * @param vec1 向量1
     * @param vec2 向量2
     * @return double
     */
    public static double manhattanDistance(List<Double> vec1, List<Double> vec2){
        return calDistance(vec1, vec2, (x, y) -> Math.abs(x - y), x -> x);
    }

    /**
     * 闵科夫斯基距离
     * @param vec1 向量1
     * @param vec2 向量2
     * @param norm 范数
     * @return double
     */
    public static double minkowskiDistance(List<Double> vec1, List<Double> vec2, int norm){
        return calDistance(vec1, vec2, (x, y) -> Math.pow(Math.abs(x - y), norm), x -> Math.pow(x, 1D / norm));
    }

    /**
     * 余弦相似度
     * @param vec1 向量1
     * @param vec2 向量2
     * @return double
     */
    public static double cosineSimilarity(List<Double> vec1, List<Double> vec2){
        check(vec1, vec2);
        double productSum = IntStream.range(0, vec1.size()).mapToDouble(idx -> vec1.get(idx) * vec2.get(idx)).sum();
        return productSum / (norm(vec1) * norm(vec2));
    }

    /**
     * 计算欧几里得范数
     * @param vec 向量
     * @return double
     */
    private static double norm(List<Double> vec){
        return vec.stream().mapToDouble(d ->  Math.pow(d, 2)).sum();
    }

}
